iT邦幫忙

2023 iThome 鐵人賽

DAY 10
0
AI & Data

生成式AI到底何方神聖?一窺生程式AI的真面目系列 第 10

[Day10]:我的第一個GAN模型

  • 分享至 

  • xImage
  •  

前言

昨天介紹了我在開發GAN模型時的一些思路以及習慣,希望這對各位在開發模型時能有幫助,接下來就馬上帶各位來實戰,親手建立一個GAN模型!

開發環境

首先再次介紹一下,我的開發環境如下:

工具名稱 版本
Python (使用conda環境) 3.8
Pycharm Professional 2021.1.2 x64

函式庫的版本如下:

函式庫名稱 函式庫版本
keras 2.6.0
tensorflow 2.6.0
tensorflow-gpu 2.6.0
scipy 1.9.3
numpy 1.19.2
matplotlib 3.5.2
pandas 1.4.2
opencv 3.4.8.29

如果忘記了這些怎麼裝可以參考我在第三天的介紹喔,若想確認版本是否正確可以使用pip list來看看所有函式庫的版本。如果都沒問題了以後就來建立GAN模型吧!

順帶一提OpenCV的安裝方式如下:後面記得加python,另外匯入函式庫的寫法是import cv2,要特別注意~

pip install opencv-python

建立GAN模型

那我們接著根據昨天的步驟一步一步的來實作GAN模型!

第一步:決定任務類型與要使用的模型

任務類型很簡單就是生成mnist圖像資料,使用的模型先使用最基本的GAN

第二步:匯入函式庫

這一步會匯入函式庫,例如TensorFlow之類的。這一次的程式會使用到的函式庫跟方法如下:

from tensorflow.keras.datasets import mnist
from tensorflow.keras.layers import Input, Dense, Reshape, Flatten, BatchNormalization, LeakyReLU
from tensorflow.keras.models import Model, save_model
from tensorflow.keras.optimizers import Adam

import matplotlib.pyplot as plt
import numpy as np
import os

第三步:資料前處理 (Data Preprocessing)

這一步就是將資料匯進來,並正規化、確認資料的shape等。因為mnist資料基本上都處理好了,所以基本上處理不會很複雜。首先直接除以255使每個像素的值都落在0~1之間,原本以圖片來說每個像素的值都是0~255,但為了不要讓數字太大導致訓練成效不彰,所以都會正規化到比較小的值域之間。接著使用reshape()方法將資料集重新塑形,shape中的-1是代表會自動計算那個位置的值,若不知道資料有幾筆可以直接在第一個元素中設定-1,另外圖片的格式是28x28x1。所以整個資料的形狀設定就是(-1, 28, 28, 1)。此時就會自動計算-1那個位置的值是多少,以本例來說是60000。

⚠️-1的用法基本上是例如今天有一個長度為6的向量,但想整理成兩列,也就是shape=(2, ?)時,若不知道每列有多少元素的,就可以設定reshape((2, -1)),此時NumPy就會自動計算6/2=3,所以整個向量就會變成 shape=(2, 3)的形式,所以當只有一項維度不確定時,就可以使用-1來自動計算該位置的值。

這個處理資料的方法在這系列文章中基本上也會放在GAN類別裡面隨call隨用~

def load_data(self):
		from tensorflow.keras.datasets import mnist
        (x_train, _), (_, _) = mnist.load_data() #底線是未被用到的資料,可忽略
        x_train = x_train / 255 #正規化
        x_train = x_train.reshape((-1, 28, 28, 1))
        return x_train

題外話,通常我在資料處理時會另外開一個py檔案專門用來建立處理資料的副程式,接著才在主程式匯入自定義的方法,讓程式碼變得比較簡潔,易於閱讀。

第四步:建立生成模型類別

這一步就是建立生成模型的類別而已,定義GAN類別,並定義初始化方法。基本上超參數設定有很多種,也可以不在此設定直接定義於程式碼內部。但我的習慣是會設定,因為在訓練時有時會使用超參數優化的優化算法來協助訓練。

優化算法本來想說一些的,但發現這30天似乎不夠用。若沒有別的主題是我更想介紹的話,沒意外的話優化算法會留給明年鐵人賽再詳細介紹!

生成器、判別器與對抗模型會在建立完成後再從初始化方法中定義模型。

class GAN():
    def __init__(self, generator_lr, discriminator_lr):
		self.generator_lr = generator_lr #生成器學習率
        self.discriminator_lr = discriminator_lr #判別器學習率

        self.discriminator = self.build_discriminator() #生成器
        self.generator = self.build_generator() #判別器
        self.adversarial = self.build_adversarialmodel() #對抗模型
		#紀錄生成器與鑑別器損失
		self.gloss = []
        self.dloss = []
		# 這邊與昨天介紹的程式不同,採用另一個建立資料夾的方法,可以一次建立多個目錄
        if not os.path.exists('./result/GAN/imgs'): #將訓練過程產生的圖片儲存起來
            os.makedirs('./result/GAN/imgs') #如果忘記新增資料夾可以用這個方式建立

第五步:建立模型、定義訓練方法

這一步是最麻煩的,但也是核心所在,首先先定義生成器、判別器、以及對抗模型,基本上建立這些模型流程都差不多。大致的SOP如下:

  1. 建立輸入層,此時要注意輸入的shape是否符合預期。
  2. 建立中間隱藏層,可根據教學、文獻、自己的想法建立。如果是定義對抗網路的話則將生成器與判別器用Functional API接起來就好了。這部分待會會細講。
  3. 建立輸出層,輸出層的激活函數要注意,生成器的激活函數根據正規化的方式設定,判別器通常都是使用Sigmoid。
  4. 將網路層用Model方法定義成完整模型。
  5. 定義模型優化器、目標函數 (損失函數)。
  6. 編譯模型,模型編譯完以後則可以確認模型的細節資訊等。

生成器:生成器的輸入因為是要憑空生圖片,所以通常會使用一段常態分佈的隱向量作為輸入。這次的程式碼隱向量長度是100。

中間隱藏層使用全連接層,激活函數是LeakyReLU(),與ReLU不同,LeakyReLU在輸入為負時也會根據設定的斜率(alpha)進行輸出,最後再經過輸出層輸出成長度為28x28x1也就是784的向量,接著就是重新塑形成圖片的格式,也就是reshape為 (28, 28, 1)。

另外生成器的模型基本上不需要編譯,因為實際訓練是用對抗模型在訓練生成器,所以就只需要編譯對抗模型就好。

def build_generator(self):
		#建立輸入層
        input_ = Input(shape=(100, ))
		#建立中間隱藏層
        x = Dense(256)(input_)
        x = LeakyReLU(alpha=0.2)(x)
        x = BatchNormalization(momentum=0.8)(x)
        x = Dense(512)(x)
        x = LeakyReLU(alpha=0.2)(x)
        x = BatchNormalization(momentum=0.8)(x)
        x = Dense(1024)(x)
        x = LeakyReLU(alpha=0.2)(x)
        x = BatchNormalization(momentum=0.8)(x)
		#建立輸出層,輸出資料要再塑形成圖片的格式
        x = Dense(28*28*1, activation='sigmoid')(x)
        out = Reshape((28,28,1))(x)
        #定義模型
        model = Model(inputs=input_, outputs=out, name='Generator')
        model.summary() #確認模型的細節資訊,非必要
        return model

判別器:判別器的輸入是一張圖片,並且在經過判別器以後要判斷這張圖片是真是假。輸入圖片的圖片shape是 (28, 28, 1)的,另外會經過Flatten()使其變成一維的向量,也就是變成長度為 (784, )的向量,接著經過兩個全連接層,兩層全連接層的激活函數是LeakyReLU()

優化器使用Adam,學習率會於之後訓練設定,beta_1是Adam的其中一個超參數,通常設定0.5會使優化過程比較平穩,讓訓練更加穩定。優化的選擇也沒有硬性規定,各位可以使用其他的優化器來訓練看看。

最後就是編譯模型,損失函數使用二元交叉熵 (binary_crossentropy),另外使用準確率當評估標準,所以在之後訓練一批次後會有兩個資料回傳,第一是損失值,第二是準確率

def build_discriminator(self):
		#建立輸入層
        input_ = Input(shape = (28, 28, 1))
		#建立中間隱藏層
        x = Flatten()(input_)
        x = Dense(512)(x)
        x = LeakyReLU(alpha=0.2)(x)
        x = Dense(256)(x)
        x = LeakyReLU(alpha=0.2)(x)
		#建立輸出層,輸出資料要再塑形成圖片的格式
        out = Dense(1, activation='sigmoid')(x)
		#定義模型
        model = Model(inputs=input_, outputs=out, name='Discriminator')
		#定義模型優化器
        dis_optimizer = Adam(learning_rate=self.discriminator_lr , beta_1=0.5)
		#編譯模型
        model.compile(loss='binary_crossentropy',
                      optimizer=dis_optimizer,
                      metrics=['accuracy'])
				model.summary() #確認模型的細節資訊,非必要
        return model

對抗模型:對抗模型基本上也比較容易建立,因為就是把兩個模型給接起來而已。首先定義輸入層,也就是雜訊輸入,接著經過生成器生成圖片,然後再把圖片輸入至判別器獲取輸出。最後就是定義優化器、編譯模型了。

一定要著以對抗模型是要訓練生成器生成可以騙過判別器的圖片,所以才要將兩個模型接在一起,讓生成器可以練習騙判別器。此時**絕對不可以訓練判別器,故要固定住判別器的權重。**固定判別器權重非常重要,建立對抗模型時要注意這點。

def build_adversarialmodel(self):
		#建立輸入層
        noise_input = Input(shape=(100, ))
		#建立中間隱藏層,即結合兩個模型
        generator_sample = self.generator(noise_input)
		#固定判別器權重,因為只需要訓練生成器
        self.discriminator.trainable = False
		#建立輸出層,也就是判別器的輸出
        out = self.discriminator(generator_sample)
		#定義模型
        model = Model(inputs=noise_input, outputs=out)
		#定義模型優化器、目標函數
        adv_optimizer = Adam(learning_rate=self.generator_lr, beta_1=0.5)
		#編譯模型
        model.compile(loss='binary_crossentropy', optimizer=adv_optimizer)
        model.summary() #確認模型的細節資訊,非必要
        return model

接著定義訓練方法,GAN的訓練方法基本上也是遵照這幾個流程:

  1. 將訓練資料準備好。
  2. 訓練判別器,並且儲存損失。
  3. 訓練生成器,並且儲存損失。
  4. 在經過特定的訓練次數會用生成器生成結果並儲存,用於訓練後分析訓練情形用的。
  5. 訓練完成後儲存訓練結果,包括模型權重檔案、損失圖、訓練過程產生的照片等。

訓練方法中epochs是訓練次數、batch_size是一批次的訓練資料量、sample_interval是設定每幾次訓練就儲存一次生成結果。

訓練判別器前會先讓生成器生成一批假的圖片以供判別器訓練判斷真假,train_on_batch(輸入, 正確標籤)方法是使模型訓練一批的資料,並計算損失。

接著在訓練生成器的生成能力的過程中,因為鑑別器有初步訓練到如何分辨資料,所以此時生成器要盡量生成讓判別器能認為是真實的圖片。所以會使用valid當作正確答案。

def train(self, epochs, batch_size=128, sample_interval=50):
        # 準備訓練資料
        x_train = self.load_data()
        # 準備訓練的標籤,分為真實標籤與假標籤
        valid = np.ones((batch_size, 1))
        fake = np.zeros((batch_size, 1))
        for epoch in range(epochs):
            # 隨機取一批次的資料用來訓練
            idx = np.random.randint(0, x_train.shape[0], batch_size)
            imgs = x_train[idx]
            # 從常態分佈中採樣一段雜訊
            noise = np.random.normal(0, 1, (batch_size, 100))
            # 生成一批假圖片
            gen_imgs = self.generator.predict(noise)
            # 判別器訓練判斷真假圖片
            d_loss_real = self.discriminator.train_on_batch(imgs, valid)
            d_loss_fake = self.discriminator.train_on_batch(gen_imgs, fake)
            d_loss = 0.5 * np.add(d_loss_real, d_loss_fake)
            self.dloss.append(d_loss[0])
            # 訓練生成器的生成能力
            noise = np.random.normal(0, 1, (batch_size, 100))
            g_loss = self.adversarial.train_on_batch(noise, valid)
            self.gloss.append(g_loss)
            # 將這一步的訓練資訊print出來
            print(f"Epoch:{epoch} [D loss: {d_loss[0]}, acc: {100 * d_loss[1]:.2f}] [G loss: {g_loss}]")
            # 在指定的訓練次數中,隨機生成圖片,將訓練過程的圖片儲存起來
            if epoch % sample_interval == 0:
                self.sample(epoch)
        self.save_data() #儲存訓練結果

定義其他副程式:

通常我會根據其他需求來定義副程式,原因很簡單,還是提升程式碼的可讀性。

首先定義隨機生成圖片的副程式sample(epoch)

 def sample(self, epoch, num_images=25):
        r = int(np.sqrt(num_images))
        noise = np.random.normal(0, 1, (num_images, 100))
        gen_imgs = self.generator.predict(noise)

        fig, axs = plt.subplots(r, r)
        count = 0
        for i in range(r):
            for j in range(r):
                axs[i,j].imshow(gen_imgs[count, :, :, 0], cmap='gray')
                axs[i,j].axis('off')
                count += 1
        fig.savefig(f"./result/GAN/imgs/{epoch}epochs.png")
        plt.close()

接著定義儲存訓練資料的副程式save_data()

def save_data(self):
        np.save(file='./result/GAN/generator_loss.npy',arr=np.array(self.gloss))
        np.save(file='./result/GAN/discriminator_loss.npy', arr=np.array(self.dloss))
        save_model(model=self.generator,filepath='./result/GAN/Generator.h5')
        save_model(model=self.discriminator,filepath='./result/GAN/Discriminator.h5')
        save_model(model=self.adversarial,filepath='./result/GAN/Adversarial.h5')

以及最後生成最終結果的副程式predict(),這方法其實跟sample()類似,只是一個存圖片一個直接顯示出來,可以把兩個副程式合併,並設定自定義的參數來控制功能。

def predict(self, num_images=25):
        r = int(np.sqrt(num_images))
        noise = np.random.normal(0, 1, (num_images, 100))
        gen_imgs = self.generator.predict(noise)

        fig, axs = plt.subplots(r, r)
        count = 0
        for i in range(r):
            for j in range(r):
                axs[i, j].imshow(gen_imgs[count, :, :, 0], cmap='gray')
                axs[i, j].axis('off')
                count += 1
        plt.show()

第六步:開始訓練

最後就是開始訓練了,直接在主程式定義GAN類別並使用train()方法訓練即可,參數設定如下。

參數名稱 參數值
生成器學習率 0.0002
判別器學習率 0.0002
Batch Size 32
訓練次數 30000
if __name__ == '__main__':
    gan = GAN(generator_lr=0.0002, discriminator_lr=0.0002)
    gan.train(epochs=30000, batch_size=32, sample_interval=200)
    gan.predict()

結語

這時候可能會想,昨天不是介紹了第七步嗎?第七步是審視訓練結果並改進,不過GAN訓練本來就會花很多時間,所以程式開始訓練了以後就放著等他訓練吧。明天再來看看我們的訓練結果,GAN在訓練過程中本來就會因為超參數的設定、模型架構、資料集的品質而受到影響進而產生很多訓練問題。今天帶各位實做了GAN,各位若有跑過程式也可以看看有出現甚麼問題 (希望是沒有),其餘具體內容明天再來討論!

附錄:完整程式

from tensorflow.keras.datasets import mnist
from tensorflow.keras.layers import Input, Dense, Reshape, Flatten, BatchNormalization, LeakyReLU
from tensorflow.keras.models import Model, save_model
from tensorflow.keras.optimizers import Adam

import matplotlib.pyplot as plt
import numpy as np
import os

class GAN():
    def __init__(self, generator_lr, discriminator_lr):
        self.generator_lr = generator_lr
        self.discriminator_lr = discriminator_lr

        self.discriminator = self.build_discriminator()
        self.generator = self.build_generator()
        self.adversarial = self.build_adversarialmodel()

        self.gloss = []
        self.dloss = []
		# 這邊與昨天介紹的程式不同,採用另一個建立資料夾的方法,可以一次建立多個目錄
        if not os.path.exists('./result/GAN/imgs'):# 將訓練過程產生的圖片儲存起來
            os.makedirs('./result/GAN/imgs')# 如果忘記新增資料夾可以用這個方式建立
    def load_data(self):
        (x_train, _), (_, _) = mnist.load_data()
        x_train = x_train / 255
        x_train = x_train.reshape((-1, 28, 28, 1))
        return x_train
    def build_generator(self):
        input_ = Input(shape=(100, ))
        x = Dense(256)(input_)
        x = LeakyReLU(alpha=0.2)(x)
        x = BatchNormalization(momentum=0.8)(x)
        x = Dense(512)(x)
        x = LeakyReLU(alpha=0.2)(x)
        x = BatchNormalization(momentum=0.8)(x)
        x = Dense(1024)(x)
        x = LeakyReLU(alpha=0.2)(x)
        x = BatchNormalization(momentum=0.8)(x)
        x = Dense(28*28*1, activation='sigmoid')(x)
        out = Reshape((28,28,1))(x)

        model = Model(inputs=input_, outputs=out, name='Generator')
        model.summary()
        return model

    def build_discriminator(self):
        input_ = Input(shape = (28, 28, 1))
        x = Flatten()(input_)
        x = Dense(512)(x)
        x = LeakyReLU(alpha=0.2)(x)
        x = Dense(256)(x)
        x = LeakyReLU(alpha=0.2)(x)
        out = Dense(1, activation='sigmoid')(x)
        model = Model(inputs=input_, outputs=out, name='Discriminator')

        dis_optimizer = Adam(learning_rate=self.discriminator_lr , beta_1=0.5)
        model.compile(loss='binary_crossentropy',
                      optimizer=dis_optimizer,
                      metrics=['accuracy'])
        return model
    def build_adversarialmodel(self):
        noise_input = Input(shape=(100, ))
        generator_sample = self.generator(noise_input)
        self.discriminator.trainable = False
        out = self.discriminator(generator_sample)
        model = Model(inputs=noise_input, outputs=out)

        adv_optimizer = Adam(learning_rate=self.generator_lr, beta_1=0.5)
        model.compile(loss='binary_crossentropy', optimizer=adv_optimizer)
        model.summary()
        return model

    def train(self, epochs, batch_size=128, sample_interval=50):
        # 準備訓練資料
        x_train = self.load_data()
        # 準備訓練的標籤,分為真實標籤與假標籤
        valid = np.ones((batch_size, 1))
        fake = np.zeros((batch_size, 1))
        for epoch in range(epochs):
            # 隨機取一批次的資料用來訓練
            idx = np.random.randint(0, x_train.shape[0], batch_size)
            imgs = x_train[idx]
            # 從常態分佈中採樣一段雜訊
            noise = np.random.normal(0, 1, (batch_size, 100))
            # 生成一批假圖片
            gen_imgs = self.generator.predict(noise)
            # 判別器訓練判斷真假圖片
            d_loss_real = self.discriminator.train_on_batch(imgs, valid)
            d_loss_fake = self.discriminator.train_on_batch(gen_imgs, fake)
            d_loss = 0.5 * np.add(d_loss_real, d_loss_fake)
            self.dloss.append(d_loss[0])
            # 訓練生成器的生成能力
            noise = np.random.normal(0, 1, (batch_size, 100))
            g_loss = self.adversarial.train_on_batch(noise, valid)
            self.gloss.append(g_loss)
            # 將這一步的訓練資訊print出來
            print(f"Epoch:{epoch} [D loss: {d_loss[0]}, acc: {100 * d_loss[1]:.2f}] [G loss: {g_loss}]")
            # 在指定的訓練次數中,隨機生成圖片,將訓練過程的圖片儲存起來
            if epoch % sample_interval == 0:
                self.sample(epoch)
        self.save_data()
    def save_data(self):
        np.save(file='./result/GAN/generator_loss.npy',arr=np.array(self.gloss))
        np.save(file='./result/GAN/discriminator_loss.npy', arr=np.array(self.dloss))
        save_model(model=self.generator,filepath='./result/GAN/Generator.h5')
        save_model(model=self.discriminator,filepath='./result/GAN/Discriminator.h5')
        save_model(model=self.adversarial,filepath='./result/GAN/Adversarial.h5')
    def sample(self, epoch, num_images=25):
        r = int(np.sqrt(num_images))
        noise = np.random.normal(0, 1, (num_images, 100))
        gen_imgs = self.generator.predict(noise)

        fig, axs = plt.subplots(r, r)
        count = 0
        for i in range(r):
            for j in range(r):
                axs[i,j].imshow(gen_imgs[count, :, :, 0], cmap='gray')
                axs[i,j].axis('off')
                count += 1
        fig.savefig(f"./result/GAN/imgs/{epoch}epochs.png")
        plt.close()
    def predict(self, num_images=25):
        r = int(np.sqrt(num_images))
        noise = np.random.normal(0, 1, (num_images, 100))
        gen_imgs = self.generator.predict(noise)

        fig, axs = plt.subplots(r, r)
        count = 0
        for i in range(r):
            for j in range(r):
                axs[i, j].imshow(gen_imgs[count, :, :, 0], cmap='gray')
                axs[i, j].axis('off')
                count += 1
        plt.show()

if __name__ == '__main__':
    gan = GAN(generator_lr=0.0002, discriminator_lr=0.0002)
    gan.train(epochs=10000, batch_size=32, sample_interval=200)
    gan.predict()

上一篇
[Day9]:生成式AI如何開發-建立GAN有甚麼SOP
下一篇
[Day11]:GAN在訓練可能會遇到的問題…
系列文
生成式AI到底何方神聖?一窺生程式AI的真面目31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言